/*
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */

package org.evosuite.instrumentation.mutation;

import org.evosuite.PackageInfo;
import org.evosuite.TestGenerationContext;
import org.evosuite.coverage.mutation.Mutation;
import org.evosuite.coverage.mutation.MutationPool;
import org.evosuite.graphs.cfg.BytecodeInstruction;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.Frame;

import java.util.*;


/**
 * <p>ReplaceArithmeticOperator class.</p>
 *
 * @author Gordon Fraser
 */
public class ReplaceArithmeticOperator implements MutationOperator {

    public static final String NAME = "ReplaceArithmeticOperator";

    private static final Set<Integer> opcodesInt = new HashSet<>();

    private static final Set<Integer> opcodesLong = new HashSet<>();

    private static final Set<Integer> opcodesFloat = new HashSet<>();

    private static final Set<Integer> opcodesDouble = new HashSet<>();

    private int numVariable = 0;

    static {
        opcodesInt.addAll(Arrays.asList(Opcodes.IADD, Opcodes.ISUB,
                Opcodes.IMUL, Opcodes.IDIV, Opcodes.IREM));

        opcodesLong.addAll(Arrays.asList(Opcodes.LADD, Opcodes.LSUB,
                Opcodes.LMUL, Opcodes.LDIV, Opcodes.LREM));

        opcodesFloat.addAll(Arrays.asList(Opcodes.FADD, Opcodes.FSUB,
                Opcodes.FMUL, Opcodes.FDIV, Opcodes.FREM));

        opcodesDouble.addAll(Arrays.asList(Opcodes.DADD, Opcodes.DSUB,
                Opcodes.DMUL, Opcodes.DDIV, Opcodes.DREM));
    }

    private String getOp(int opcode) {
        switch (opcode) {
            case Opcodes.IADD:
            case Opcodes.LADD:
            case Opcodes.FADD:
            case Opcodes.DADD:
                return "+";
            case Opcodes.ISUB:
            case Opcodes.LSUB:
            case Opcodes.FSUB:
            case Opcodes.DSUB:
                return "-";
            case Opcodes.IMUL:
            case Opcodes.LMUL:
            case Opcodes.FMUL:
            case Opcodes.DMUL:
                return "*";
            case Opcodes.IDIV:
            case Opcodes.LDIV:
            case Opcodes.FDIV:
            case Opcodes.DDIV:
                return "/";
            case Opcodes.IREM:
            case Opcodes.LREM:
            case Opcodes.FREM:
            case Opcodes.DREM:
                return "%";
        }
        throw new RuntimeException("Unknown opcode: " + opcode);
    }

    /**
     * <p>getNextIndex</p>
     *
     * @param mn a {@link org.objectweb.asm.tree.MethodNode} object.
     * @return a int.
     */
    public static int getNextIndex(MethodNode mn) {
        Iterator<LocalVariableNode> it = mn.localVariables.iterator();
        int max = 0;
        int next = 0;
        while (it.hasNext()) {
            LocalVariableNode var = it.next();
            int index = var.index;
            if (index >= max) {
                max = index;
                next = max + Type.getType(var.desc).getSize();
            }
        }
        if (next == 0)
            next = getNextIndexFromLoad(mn);
        return next;
    }

    private static int getNextIndexFromLoad(MethodNode mn) {
        Iterator<AbstractInsnNode> it = mn.instructions.iterator();
        int index = 0;
        while (it.hasNext()) {
            AbstractInsnNode node = it.next();
            if (node instanceof VarInsnNode) {
                VarInsnNode varNode = (VarInsnNode) node;
                int varIndex = varNode.var;
                switch (varNode.getOpcode()) {
                    case Opcodes.ALOAD:
                    case Opcodes.ILOAD:
                    case Opcodes.FLOAD:
                    case Opcodes.IALOAD:
                    case Opcodes.BALOAD:
                    case Opcodes.CALOAD:
                    case Opcodes.AALOAD:
                    case Opcodes.ASTORE:
                    case Opcodes.ISTORE:
                    case Opcodes.FSTORE:
                    case Opcodes.IASTORE:
                    case Opcodes.BASTORE:
                    case Opcodes.CASTORE:
                    case Opcodes.AASTORE:
                        index = Math.max(index, varIndex + 1);
                        break;
                    case Opcodes.DLOAD:
                    case Opcodes.DSTORE:
                    case Opcodes.LLOAD:
                    case Opcodes.LSTORE:
                    case Opcodes.DALOAD:
                    case Opcodes.DASTORE:
                    case Opcodes.LALOAD:
                    case Opcodes.LASTORE:
                        index = Math.max(index, varIndex + 2);
                        break;
                }
            }
        }

        return index;
    }

    /* (non-Javadoc)
     * @see org.evosuite.cfg.instrumentation.MutationOperator#apply(org.objectweb.asm.tree.MethodNode, java.lang.String, java.lang.String, org.evosuite.cfg.BytecodeInstruction)
     */

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Mutation> apply(MethodNode mn, String className, String methodName,
                                BytecodeInstruction instruction, Frame frame) {

        numVariable = getNextIndex(mn);
        List<Mutation> mutations = new LinkedList<>();

        InsnNode node = (InsnNode) instruction.getASMNode();

        for (int opcode : getMutations(node.getOpcode())) {

            InsnNode mutation = new InsnNode(opcode);
            // insert mutation into pool
            Mutation mutationObject = MutationPool.getInstance(TestGenerationContext.getInstance().getClassLoaderForSUT()).addMutation(className,
                    methodName,
                    NAME + " "
                            + getOp(node.getOpcode())
                            + " -> "
                            + getOp(opcode),
                    instruction,
                    mutation,
                    getInfectionDistance(node.getOpcode(),
                            opcode));
            mutations.add(mutationObject);
        }

        return mutations;
    }

    private Set<Integer> getMutations(int opcode) {
        Set<Integer> replacement = new HashSet<>();
        if (opcodesInt.contains(opcode))
            replacement.addAll(opcodesInt);
        else if (opcodesLong.contains(opcode))
            replacement.addAll(opcodesLong);
        else if (opcodesFloat.contains(opcode))
            replacement.addAll(opcodesFloat);
        else if (opcodesDouble.contains(opcode))
            replacement.addAll(opcodesDouble);

        replacement.remove(opcode);
        return replacement;
    }

    /**
     * <p>getInfectionDistance</p>
     *
     * @param opcodeOrig a int.
     * @param opcodeNew  a int.
     * @return a {@link org.objectweb.asm.tree.InsnList} object.
     */
    public InsnList getInfectionDistance(int opcodeOrig, int opcodeNew) {
        InsnList distance = new InsnList();

        if (opcodesInt.contains(opcodeOrig)) {
            distance.add(new InsnNode(Opcodes.DUP2));
            distance.add(new LdcInsnNode(opcodeOrig));
            distance.add(new LdcInsnNode(opcodeNew));
            distance.add(new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    PackageInfo.getNameWithSlash(ReplaceArithmeticOperator.class),
                    "getInfectionDistanceInt", "(IIII)D", false));
        } else if (opcodesLong.contains(opcodeOrig)) {
            distance.add(new VarInsnNode(Opcodes.LSTORE, numVariable));
            distance.add(new InsnNode(Opcodes.DUP2));
            distance.add(new VarInsnNode(Opcodes.LLOAD, numVariable));
            distance.add(new InsnNode(Opcodes.DUP2_X2));
            distance.add(new LdcInsnNode(opcodeOrig));
            distance.add(new LdcInsnNode(opcodeNew));
            distance.add(new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    PackageInfo.getNameWithSlash(ReplaceArithmeticOperator.class),
                    "getInfectionDistanceLong", "(JJII)D", false));
            numVariable += 2;
        } else if (opcodesFloat.contains(opcodeOrig)) {
            distance.add(new InsnNode(Opcodes.DUP2));
            distance.add(new LdcInsnNode(opcodeOrig));
            distance.add(new LdcInsnNode(opcodeNew));
            distance.add(new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    PackageInfo.getNameWithSlash(ReplaceArithmeticOperator.class),
                    "getInfectionDistanceFloat", "(FFII)D", false));
        } else if (opcodesDouble.contains(opcodeOrig)) {
            distance.add(new VarInsnNode(Opcodes.DSTORE, numVariable));
            distance.add(new InsnNode(Opcodes.DUP2));
            distance.add(new VarInsnNode(Opcodes.DLOAD, numVariable));
            distance.add(new InsnNode(Opcodes.DUP2_X2));
            distance.add(new LdcInsnNode(opcodeOrig));
            distance.add(new LdcInsnNode(opcodeNew));
            distance.add(new MethodInsnNode(
                    Opcodes.INVOKESTATIC,
                    PackageInfo.getNameWithSlash(ReplaceArithmeticOperator.class),
                    "getInfectionDistanceDouble", "(DDII)D", false));
            numVariable += 2;
        }

        return distance;
    }

    private static boolean hasDivZeroError(int opcode) {
        switch (opcode) {
            case Opcodes.IDIV:
            case Opcodes.IREM:
            case Opcodes.FDIV:
            case Opcodes.FREM:
            case Opcodes.LDIV:
            case Opcodes.LREM:
            case Opcodes.DDIV:
            case Opcodes.DREM:
                return true;
            default:
                return false;
        }
    }

    /**
     * <p>getInfectionDistanceInt</p>
     *
     * @param x          a int.
     * @param y          a int.
     * @param opcodeOrig a int.
     * @param opcodeNew  a int.
     * @return a double.
     */
    public static double getInfectionDistanceInt(int x, int y, int opcodeOrig,
                                                 int opcodeNew) {
        if (y == 0) {
            return hasDivZeroError(opcodeOrig) == hasDivZeroError(opcodeNew) ? 1.0 : 0.0;
        }
        int origValue = calculate(x, y, opcodeOrig);
        int newValue = calculate(x, y, opcodeNew);
        return origValue == newValue ? 1.0 : 0.0;
    }

    /**
     * <p>getInfectionDistanceLong</p>
     *
     * @param x          a long.
     * @param y          a long.
     * @param opcodeOrig a int.
     * @param opcodeNew  a int.
     * @return a double.
     */
    public static double getInfectionDistanceLong(long x, long y, int opcodeOrig,
                                                  int opcodeNew) {
        if (y == 0L) {
            return hasDivZeroError(opcodeOrig) == hasDivZeroError(opcodeNew) ? 1.0 : 0.0;
        }
        long origValue = calculate(x, y, opcodeOrig);
        long newValue = calculate(x, y, opcodeNew);
        return origValue == newValue ? 1.0 : 0.0;
    }

    /**
     * <p>getInfectionDistanceFloat</p>
     *
     * @param x          a float.
     * @param y          a float.
     * @param opcodeOrig a int.
     * @param opcodeNew  a int.
     * @return a double.
     */
    public static double getInfectionDistanceFloat(float x, float y, int opcodeOrig,
                                                   int opcodeNew) {
        if (y == 0.0F) {
            return hasDivZeroError(opcodeOrig) == hasDivZeroError(opcodeNew) ? 1.0 : 0.0;
        }
        float origValue = calculate(x, y, opcodeOrig);
        float newValue = calculate(x, y, opcodeNew);
        return origValue == newValue ? 1.0 : 0.0;
    }

    /**
     * <p>getInfectionDistanceDouble</p>
     *
     * @param x          a double.
     * @param y          a double.
     * @param opcodeOrig a int.
     * @param opcodeNew  a int.
     * @return a double.
     */
    public static double getInfectionDistanceDouble(double x, double y, int opcodeOrig,
                                                    int opcodeNew) {
        if (y == 0.0) {
            return hasDivZeroError(opcodeOrig) == hasDivZeroError(opcodeNew) ? 1.0 : 0.0;
        }
        double origValue = calculate(x, y, opcodeOrig);
        double newValue = calculate(x, y, opcodeNew);
        return origValue == newValue ? 1.0 : 0.0;
    }

    /**
     * <p>calculate</p>
     *
     * @param x      a int.
     * @param y      a int.
     * @param opcode a int.
     * @return a int.
     */
    public static int calculate(int x, int y, int opcode) {
        switch (opcode) {
            case Opcodes.IADD:
                return x + y;
            case Opcodes.ISUB:
                return x - y;
            case Opcodes.IMUL:
                return x * y;
            case Opcodes.IDIV:
                return x / y;
            case Opcodes.IREM:
                return x % y;
        }
        throw new RuntimeException("Unknown integer opcode: " + opcode);
    }

    /**
     * <p>calculate</p>
     *
     * @param x      a long.
     * @param y      a long.
     * @param opcode a int.
     * @return a long.
     */
    public static long calculate(long x, long y, int opcode) {
        switch (opcode) {
            case Opcodes.LADD:
                return x + y;
            case Opcodes.LSUB:
                return x - y;
            case Opcodes.LMUL:
                return x * y;
            case Opcodes.LDIV:
                return x / y;
            case Opcodes.LREM:
                return x % y;
        }
        throw new RuntimeException("Unknown integer opcode: " + opcode);
    }

    /**
     * <p>calculate</p>
     *
     * @param x      a float.
     * @param y      a float.
     * @param opcode a int.
     * @return a float.
     */
    public static float calculate(float x, float y, int opcode) {
        switch (opcode) {
            case Opcodes.FADD:
                return x + y;
            case Opcodes.FSUB:
                return x - y;
            case Opcodes.FMUL:
                return x * y;
            case Opcodes.FDIV:
                return x / y;
            case Opcodes.FREM:
                return x % y;
        }
        throw new RuntimeException("Unknown integer opcode: " + opcode);
    }

    /**
     * <p>calculate</p>
     *
     * @param x      a double.
     * @param y      a double.
     * @param opcode a int.
     * @return a double.
     */
    public static double calculate(double x, double y, int opcode) {
        switch (opcode) {
            case Opcodes.DADD:
                return x + y;
            case Opcodes.DSUB:
                return x - y;
            case Opcodes.DMUL:
                return x * y;
            case Opcodes.DDIV:
                return x / y;
            case Opcodes.DREM:
                return x % y;
        }
        throw new RuntimeException("Unknown integer opcode: " + opcode);
    }

    /* (non-Javadoc)
     * @see org.evosuite.cfg.instrumentation.mutation.MutationOperator#isApplicable(org.evosuite.cfg.BytecodeInstruction)
     */

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isApplicable(BytecodeInstruction instruction) {
        AbstractInsnNode node = instruction.getASMNode();
        int opcode = node.getOpcode();
        if (opcodesInt.contains(opcode))
            return true;
        else if (opcodesLong.contains(opcode))
            return true;
        else if (opcodesFloat.contains(opcode))
            return true;
        else return opcodesDouble.contains(opcode);
    }
}
